import type { SourceSlideInfo } from '@slidev/types'
import type { DecorationOptions } from 'vscode'
import { clamp, debounce, ensurePrefix } from '@antfu/utils'
import { computed, defineService, onScopeDispose, useActiveTextEditor, watch } from 'reactive-vscode'
import { Position, Range, ThemeColor, window, workspace } from 'vscode'
import { useProjectFromDoc } from '../composables/useProjectFromDoc'
import { displayAnnotations, displayCodeBlockLineNumbers } from '../configs'
import { activeProject } from '../projects'
import { toRelativePath } from '../utils/toRelativePath'

const frontmatterBgOptions = {
  isWholeLine: true,
  backgroundColor: '#8881',
}

const firstLineDecoration = window.createTextEditorDecorationType({})
const dividerDecoration = window.createTextEditorDecorationType({
  ...frontmatterBgOptions,
  color: new ThemeColor('panelTitle.inactiveForeground'),
  borderStyle: 'solid',
  borderWidth: '1px 0 0 0',
  borderColor: new ThemeColor('panelTitle.activeBorder'),
})
const frontmatterContentDecoration = window.createTextEditorDecorationType(frontmatterBgOptions)
const frontmatterEndDecoration = window.createTextEditorDecorationType({
  ...frontmatterBgOptions,
  color: new ThemeColor('panelTitle.inactiveForeground'),
})
const errorDecoration = window.createTextEditorDecorationType({
  isWholeLine: true,
})
const codeBlockLineNumberDecoration = window.createTextEditorDecorationType({
  isWholeLine: false,
  rangeBehavior: 1,
})

function mergeSlideNumbers(slides: { index: number }[]): string {
  const indexes = slides.map(s => s.index + 1)
  const merged = [[indexes[0], indexes[0]]]
  for (let i = 1; i < indexes.length; i++) {
    if (merged[merged.length - 1][1] + 1 === indexes[i])
      merged[merged.length - 1][1] = indexes[i]
    else
      merged.push([indexes[i], indexes[i]])
  }
  return merged.map(([start, end]) => start === end ? `#${start}` : `#${start}-${end}`).join(', ')
}

interface CodeBlockInfo {
  startLine: number
  endLine: number
  indent: string
}

function findCodeBlocks(docText: string): CodeBlockInfo[] {
  const lines = docText.split(/\r?\n/)
  const codeBlocks: CodeBlockInfo[] = []

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    const trimmedLine = line.trimStart()

    if (trimmedLine.startsWith('```')) {
      const indent = line.slice(0, line.length - trimmedLine.length)
      const codeBlockLevel = line.match(/^\s*`+/)![0]
      const backtickCount = codeBlockLevel.trim().length
      const startLine = i

      if (backtickCount !== 3) {
        continue
      }

      let endLine = i
      for (let j = i + 1; j < lines.length; j++) {
        if (lines[j].startsWith(codeBlockLevel)) {
          endLine = j
          break
        }
      }

      if (endLine > startLine) {
        codeBlocks.push({
          startLine: startLine + 1,
          endLine,
          indent,
        })
      }

      i = endLine
    }
  }

  return codeBlocks
}

function updateCodeBlockLineNumbers(editor: ReturnType<typeof useActiveTextEditor>['value'], docText: string) {
  if (!editor || !displayCodeBlockLineNumbers.value)
    return

  const codeBlockLineNumbers: DecorationOptions[] = []
  const codeBlocks = findCodeBlocks(docText)

  for (const block of codeBlocks) {
    const lineCount = block.endLine - block.startLine
    const maxLineNumber = lineCount
    const numberWidth = String(maxLineNumber).length

    for (let i = 0; i < lineCount; i++) {
      const lineNumber = i + 1
      const currentLine = block.startLine + i

      if (currentLine >= editor.document.lineCount)
        continue

      // \u2800 renders as a space but won't be trimmed
      const paddedNumber = String(lineNumber).padStart(numberWidth, '\u2800')

      codeBlockLineNumbers.push({
        range: new Range(
          new Position(currentLine, 0),
          new Position(currentLine, 0),
        ),
        renderOptions: {
          before: {
            contentText: `${paddedNumber}│`,
            color: new ThemeColor('editorLineNumber.foreground'),
            fontWeight: 'normal',
            fontStyle: 'normal',
            margin: '0 0.5em 0 0',
          },
        },
      })
    }
  }

  editor.setDecorations(codeBlockLineNumberDecoration, codeBlockLineNumbers)
}

export const useAnnotations = defineService(() => {
  const editor = useActiveTextEditor()
  const doc = computed(() => editor.value?.document)
  const projectInfo = useProjectFromDoc(doc)

  let debouncedUpdateLineNumbers: ((docText: string) => void) | null = null

  watch(
    [editor, displayCodeBlockLineNumbers],
    ([currentEditor, lineNumbersEnabled]) => {
      debouncedUpdateLineNumbers = null

      if (!currentEditor || !lineNumbersEnabled) {
        if (currentEditor)
          currentEditor.setDecorations(codeBlockLineNumberDecoration, [])
        return
      }

      debouncedUpdateLineNumbers = debounce(150, (docText: string) => {
        if (editor.value === currentEditor)
          updateCodeBlockLineNumbers(currentEditor, docText)
      })
    },
    { immediate: true },
  )

  const textChangeDisposable = workspace.onDidChangeTextDocument((e) => {
    if (editor.value?.document === e.document && displayCodeBlockLineNumbers.value && debouncedUpdateLineNumbers) {
      debouncedUpdateLineNumbers(e.document.getText())
    }
  })

  onScopeDispose(() => {
    textChangeDisposable.dispose()
  })

  watch(
    [editor, doc, projectInfo, activeProject, displayAnnotations, displayCodeBlockLineNumbers],
    ([editor, doc, projectInfo, activeProject, enabled, lineNumbersEnabled]) => {
      if (!editor || !doc || !projectInfo)
        return

      if (!enabled) {
        editor.setDecorations(firstLineDecoration, [])
        editor.setDecorations(dividerDecoration, [])
        editor.setDecorations(frontmatterContentDecoration, [])
        editor.setDecorations(frontmatterEndDecoration, [])
        editor.setDecorations(errorDecoration, [])
        editor.setDecorations(codeBlockLineNumberDecoration, [])
        return
      }

      const { project, md } = projectInfo
      const isActive = project === activeProject
      const docText = doc.getText()

      function getTextContent(source: SourceSlideInfo) {
        const slides = project.data.slides.filter(
          s => s.source === source || s.importChain?.includes(source),
        )
        const posInfo = slides?.length
          ? mergeSlideNumbers(slides)
          : isActive ? '(hidden)' : ''
        const entryInfo = source.index === 0 && project.data.entry !== md
          ? ` (entry: ${toRelativePath(project.entry)})`
          : ''
        const activeInfo = source.index === 0 && !isActive
          ? ' (inactive)'
          : ''
        return ` ${posInfo}${entryInfo}${activeInfo}`
      }

      const firstLineRanges: DecorationOptions[] = []
      const dividerRanges: DecorationOptions[] = []
      const frontmatterContentRanges: DecorationOptions[] = []
      const frontmatterEndRanges: DecorationOptions[] = []
      for (const slide of md.slides) {
        const lineNo = slide.frontmatterStyle === 'frontmatter' ? slide.start : slide.start - 1
        const line = doc.lineAt(clamp(lineNo, 0, doc.lineCount))

        const start = new Position(line.lineNumber, 0)
        const startDividerRange = new Range(start, new Position(line.lineNumber, line.text.length))

        // If a markdown has no headmatter, we should set `isWholeLine` to `false` for the first line
        const isSpecialCase = slide.index === 0 && !slide.frontmatterStyle
        const ranges = isSpecialCase ? firstLineRanges : dividerRanges
        ranges.push({
          range: startDividerRange,
          renderOptions: {
            after: {
              contentText: getTextContent(slide),
              fontWeight: 'bold',
              color: new ThemeColor('panelTitle.activeBorder'),
            },
          },
        })

        if (slide.frontmatterRaw != null) {
          const range = docText.slice(doc.offsetAt(start))
          const match = range.match(/^---[\s\S]*?\n---/)
          if (match && match.index != null) {
            const endLine = doc.positionAt(doc.offsetAt(start) + match.index + match[0].length).line
            frontmatterContentRanges.push({
              range: new Range(new Position(line.lineNumber + 1, 0), new Position(endLine - 1, 0)),
            })
            if (endLine !== start.line) {
              frontmatterEndRanges.push({
                range: new Range(new Position(endLine, 0), new Position(endLine, 0)),
              })
            }
          }
        }
      }

      editor.setDecorations(firstLineDecoration, firstLineRanges)
      editor.setDecorations(dividerDecoration, dividerRanges)
      editor.setDecorations(frontmatterContentDecoration, frontmatterContentRanges)
      editor.setDecorations(frontmatterEndDecoration, frontmatterEndRanges)

      const errors: DecorationOptions[] = []
      for (const error of md.errors ?? []) {
        errors.push({
          range: new Range(error.row, 0, error.row, 0),
          renderOptions: {
            after: {
              contentText: ensurePrefix(error.message, ' '),
              color: new ThemeColor('editorError.foreground'),
              backgroundColor: new ThemeColor('editorError.background'),
            },
          },
        })
      }
      editor.setDecorations(errorDecoration, errors)

      if (lineNumbersEnabled) {
        updateCodeBlockLineNumbers(editor, docText)
      }
      else {
        editor.setDecorations(codeBlockLineNumberDecoration, [])
      }
    },
    { immediate: true },
  )
})
